package redis

/*
基于 Redis 的分布式服务定位器，核心作用是作为
“服务列表的存储中枢” 和 “负载均衡器的数据源”， （存放服务消息，更新服务消（有新服务发布需要记录））
衔接 Redis（分布式存储）与负载均衡器（locate包）
*/
import (
	"context"           // 上下文管理（用于Redis操作的超时、取消）
	"encoding/json"     // JSON序列化/反序列化（Redis存储字符串，需转换结构体）
	"gameServer/locate" // 导入负载均衡器包（依赖其Balancer接口）
	"gameServer/log"
	"gameServer/registry" // 导入服务注册中心包（依赖ServiceInfo结构体）
	"time"

	"github.com/redis/go-redis/v9" // Redis官方Go客户端（处理Redis连接、增删改查、订阅发布）
)

/* //* 基于Redis的分布式服务定位器实现，提供服务发现、注册、更新功能，作为服务列表存储中枢和负载均衡器数据源 */

// Redis服务定位器 ：衔接Redis与负载均衡器，管理服务列表的获取、更新、选择
type RedisLocator struct {
	Addr      string                       // ip:port redis服务器的addr
	client    *redis.Client                // Redis客户端实例（用于操作Redis：存/取/订阅/发布）
	balancer  locate.Balancer              // 负载均衡器（接口类型，支持轮询/随机/路由算法，实现多态）
	serviceCh chan []*registry.ServiceInfo // 服务列表通道（缓冲大小10，预留用于异步传递服务更新）
	ctx       context.Context              // 上下文
	cancel    context.CancelFunc           // 取消函数，可以调用关闭
}

// 创建Redis服务定位器
//
// 参数：
//
//	addr string：Redis服务器地址（如"127.0.0.1:6379"，格式为"host:port"）
//
// 返回值：
//
//	*RedisLocator：初始化后的定位器指针（可直接调用其方法）
func NewRedisLocator(addr string) *RedisLocator {

	client := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: "", // 无密码
		DB:       0,  // 使用Redis默认数据库（Redis默认有16个库，编号0-15）
	})

	ctx, cancel := context.WithCancel(context.Background())
	// 创建RedisLocator实例，初始化核心字段
	locator := &RedisLocator{
		Addr:      addr,
		client:    client,                                 // 绑定Redis客户端
		balancer:  locate.NewRoundRobinBalancer(),         // 默认使用轮询算法
		serviceCh: make(chan []*registry.ServiceInfo, 10), // 初始化服务通道（缓冲10，避免阻塞）
		ctx:       ctx,
		cancel:    cancel,
	}
	// 测试redis 链接
	// Ping函数建立了tcp长连接，redis用了惰性链接，只有使用具体功能的时候才会从连接池提取出空闲长连接
	// 用完把链接放回去，但是tcp链接是一直保持的，比如用完set，会放回去的
	if err := client.Ping(ctx).Err(); err != nil {
		log.Errorf("Warning: Failed to connect to Redis: %v", err)
	} else {
		log.Info("Connected to Redis successfully")
	}
	// 启动协程监听服务变化（watchServerices是阻塞方法，用协程避免阻塞构造函数
	// go locator.watchServerices()
	return locator
}

// SetBalancer 动态设置负载均衡算法（替换默认的轮询算法）
//
// 参数：
//
//	balancer locate.Balancer：实现了locate.Balancer接口的负载均衡器（如随机/路由算法）
//
// 返回值：
func (l *RedisLocator) SetBalancer(balancer locate.Balancer) *RedisLocator {
	l.balancer = balancer
	// 返回自身指针，支持链式调用
	return l
}

//

// 选择服务节点：根据服务名和路由ID，从负载均衡器获取选中的节点
//
// 参数：
//
//	serviceName string：目标服务名（如"chat"、"game"，筛选同一类型的服务节点）
//	route uint32：业务路由ID（如1001=发送聊天消息，算法不同对该参数的使用不同：轮询忽略，路由算法依赖）
//
// 返回值：
//
//	*registry.ServiceInfo：选中的服务节点信息（含ID、地址、端口、元数据等）
//	error：选择过程中的错误（如无可用服务、负载均衡器未初始化）
func (l *RedisLocator) Select(serviceName string, route uint32) (*registry.ServiceInfo, error) {
	return l.balancer.Select(serviceName, route)
}

// 启动定位器
func (l *RedisLocator) Start() error {
	// 启动监听服务
	go l.watchServerices()
	log.Info("Redis locator started")
	return nil
}

// 停止定位器
func (l *RedisLocator) Stop() error {
	if l.cancel != nil {
		l.cancel()
	}
	if l.client != nil {
		return l.client.Close()
	}
	return nil
}

// GetAddr 获取Redis地址
func (l *RedisLocator) GetAddr() string {
	return l.Addr
}

// 监听服务变化 通过Redis Pub/Sub订阅服务更新频道，实时同步服务列表
func (l *RedisLocator) watchServerices() {
	// 创建基础上下文（context.Background()：无超时、无取消信号，用于Redis订阅操作）
	// ctx := context.Background()
	// 订阅Redis的"service_updates"频道（服务变化频道）（Pub/Sub模式：发布者发消息，订阅者收消息）
	// pubsub：Redis订阅实例，用于接收该频道的消息
	pubsub := l.client.Subscribe(l.ctx, "service_updates")
	/*
		Redis Pub/Sub 机制说明：
		发布者（如注册中心）通过client.Publish(ctx, "service_updates", "update")向频道发消息；
		订阅者（定位器）通过client.Subscribe订阅频道，调用ReceiveMessage阻塞接收消息；
		该机制用于 “分布式同步”：当服务列表变化时，发布者发通知，所有订阅者同步更新，避免轮询 Redis 的性能损耗
	*/
	// 延迟关闭订阅实例
	defer pubsub.Close()

	// 初始获取所有服务
	// 初始加载服务列表（服务启动时，先从Redis获取一次所有服务，避免空列表）
	l.updateServicesFromRedis()

	// 监听频道信息（无限循环，持续接收服务更新通知）
	for {
		/*
			pubsub.ReceiveMessage(ctx)：返回*redis.Message（含Channel频道名、Payload消息内容）和error，阻塞直到有消息；
			time.Sleep(1 * time.Second)：错误重试策略，避免 Redis 连接暂时断开时，定位器频繁打印错误日志（降低日志冗余）；
		*/
		// 阻塞接收变化消息
		msg, err := pubsub.ReceiveMessage(l.ctx)
		if err != nil {
			log.Errorf("Failed to receive message: %v", err)
			time.Sleep(1 * time.Second)
			continue
		}
		// 这里接收了消息，用ctx，因此需要更新ctx
		// 验证消息频道：只处理"service_updates"频道的消息（避免处理其他频道的无关消息）
		if msg.Channel == "service_updates" {
			// 调用updateServices更新服务列表（从Redis重新获取最新服务）
			l.updateServicesFromRedis()
		}
	}
}

// 更新服务列表（外调）用于更新服务列表
//
// 功能：将服务列表同步到负载均衡器、存储到Redis，并发布更新通知
// 参数：
//
//	services：需要更新的服务信息列表，类型为[]*registry.ServiceInfo（服务信息指针切片）
//
// 返回值：error类型，操作成功返回nil，失败返回具体错误信息
func (l *RedisLocator) UpdateServices(services []*registry.ServiceInfo) error {
	// 实现这个函数，继承Locator接口

	// 更新负载均衡器 让负载均衡器感知最新的服务节点，用于后续请求分发
	l.balancer.Update(services)
	// 发布到redis（如果redis可用）
	servicesJSON, err := json.Marshal(services)
	if err != nil {
		return err
	}
	// 存储服务列表到redis
	// 封装了 Redis 的SET命令，用于将服务列表持久化到 Redis，键为"services"，值为 JSON 格式的服务数据。
	if err := l.client.Set(l.ctx, "services", servicesJSON, 0).Err(); err != nil {
		log.Errorf("Failed to update services in Redis: %v", err)
		return err
	}
	// 发布更新通知
	//  Redis 的发布订阅（Pub/Sub）功能中的核心指令对应的客户端方
	// 向 Redis 中名为 service_updates 的频道（channel）发送一条消息 update
	// 所有订阅了 service_updates 频道的客户端都会收到这条消息，对应类型map[string]chan
	if err := l.client.Publish(l.ctx, "service_updates", "update").Err(); err != nil {
		log.Errorf("Failed to publish service update: %v", err)
		return err
	}
	log.Infof("Services updated in Redis: %d services", len(services))
	return nil
}

// updateServices 更新服务列表：从Redis读取最新服务列表，反序列化后同步给负载均衡器
//
// 参数：
//
//	ctx context.Context：上下文（用于Redis操作的超时控制，当前用基础上下文）
func (l *RedisLocator) updateServicesFromRedis() {
	// 从redis获取所有服务  获取"services"键的值（该键存储服务列表的JSON字符串）
	// client.Get(ctx, "services")：获取键值，Result()返回"值字符串"和"错误"
	servicesJson, err := l.client.Get(l.ctx, "services").Result()
	// 处理Redis获取错误：
	//    - 若错误是redis.Nil（键不存在，即无服务列表）：忽略（不报错，返回即可）
	//    - 其他错误（如Redis连接失败、权限不足）：日志打印，返回
	if err != nil && err != redis.Nil {
		log.Errorf("Failed to get services from Redis: %v", err)
		return
	}
	// 若Redis中"services"键的值为空（无服务）：返回（无需更新）
	if servicesJson == "" {
		return
	}
	// 将JSON字符串反序列化为[]*registry.ServiceInfo（服务列表结构体切片）
	//    - 原因：Redis存储的是字符串，需转为结构体才能被负载均衡器使用
	//    - json.Unmarshal参数：① 二进制JSON数据（[]byte(servicesJson)）；② 接收结构体指针（&services）
	//    - 注意，这里的流程是先发布服务，让redis存储起来，然后获取和更新
	var services []*registry.ServiceInfo
	if err := json.Unmarshal([]byte(servicesJson), &services); err != nil {
		// 反序列化错误（如JSON格式非法）：日志打印，返回
		log.Errorf("Failed to unmarshal services: %v", err)
		return
	}

	// 更新负载均衡器 调用负载均衡器的Update方法，同步最新服务列表（负载均衡器后续选节点用新列表）
	l.balancer.Update(services)
	log.Infof("Services updated: %d services available", len(services))
}

// PublishServices 发布服务列表：将服务列表序列化为JSON，存入Redis并通知订阅者
// 适用场景：注册中心更新服务列表后，调用该方法同步到Redis，供所有定位器感知（继承registry中的Locator接口）
//
// 参数：
//
//	ctx context.Context：上下文（用于Redis操作的超时控制）
//	services []*registry.ServiceInfo：需要发布的最新服务列表（如新增/删除节点后的列表）
//
// 返回值：
//
//	error：发布过程中的错误（如序列化失败、Redis存储失败、发布消息失败）
func (l *RedisLocator) PublishServices(ctx context.Context, services []*registry.ServiceInfo) error {
	// 将服务列表序列化为JSON字符串（结构体→JSON，便于Redis存储）
	servicesJson, err := json.Marshal(services)
	if err != nil {
		return err
	}
	// 存储服务列表到Redis
	// 将JSON字符串存入Redis的"services"键（过期时间0表示永久存储，除非手动删除）
	// client.Set(ctx, key, value, expiration)：Set方法设置键值，Err()返回错误
	if err := l.client.Set(ctx, "services", servicesJson, 0).Err(); err != nil {
		return err
	}
	// 发布更新通知 向Redis的"service_updates"频道发布消息（内容"update"），通知所有订阅者更新
	// client.Publish(ctx, channel, message)：Publish方法发布消息，Err()返回错误
	if err := l.client.Publish(ctx, "service_updates", "update").Err(); err != nil {
		return err
	}
	return nil
	// 定位器的 “数据发布接口”，主要供注册中心调用：当注册中心新增 / 删除节点后，
	// 调用该方法将最新服务列表存入 Redis，并通知所有定位器更新，实现 “一处更新，全局同步”。
}

// 关闭定位器
func (l *RedisLocator) Close() error {
	// 调用Redis客户端的Close方法，关闭与Redis的连接
	return l.client.Close()
}
